/*
 * Copyright (c) 2011, Jordan Taekema
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are
 * permitted provided that the following conditions are met:
 *
 *    1. Redistributions of source code must retain the above copyright notice, this list of
 *       conditions and the following disclaimer.
 *
 *    2. Redistributions in binary form must reproduce the above copyright notice, this list
 *       of conditions and the following disclaimer in the documentation and/or other materials
 *       provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.andactive;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

import java.lang.reflect.Field;
import java.util.*;

/**
 * Created 7/30/11 2:40 AM by Jordan
 */
@SuppressWarnings("unused")
final class DatabaseHelper extends SQLiteOpenHelper {

	/**
	 * For logging
	 */
	private static final String TAG = ActiveRecord.class.getName();

	/**
	 * Irregular plural form's of nouns.
	 */
	private static final HashMap<String, String> irregularPlural = new HashMap() {
		{
			put("PERSON", "PEOPLE");
			put("MAN", "MEN");
			put("WOMAN", "WOMEN");
			put("CHILD", "CHILDREN");
			put("COW", "KINE");
			put("FOOT", "FEET");
			put("TOOTH", "TEETH");
			put("MOUSE", "MICE");
		}
	};

	/**
	 * Irregular singular form's of nouns.
	 */
	private static final HashMap<String, String> irregularSingular = new HashMap() {
		{
			put("PEOPLE", "PERSON");
			put("MEN", "MAN");
			put("WOMEN", "WOMAN");
			put("CHILDREN", "CHILD");
			put("KINE", "COW");
			put("FEET", "FOOT");
			put("TEETH", "TOOTH");
			put("MICE", "MOUSE");
		}
	};

	/**
	 * Nouns that are both singular and plural.
	 */
	private static final HashSet<String> irregularBoth = new HashSet() {
		{
			add("EQUIPMENT");
			add("INFORMATION");
			add("RICE");
			add("MONEY");
			add("SPECIES");
			add("SERIES");
			add("FISH");
			add("SHEEP");
			add("DEER");
		}
	};

	/**
	 * Application
	 */
//	private Application app;

	/**
	 * Constructor
	 *
	 * @param context
	 */
	DatabaseHelper(Context context) {
		super(context.getApplicationContext(), ActiveRecordConfig.DB_NAME, null, ActiveRecordConfig.DB_VERSION);
//		this.app = (Application) context.getApplicationContext();
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
//		Log.i(TAG, "DatabaseHelper.onCreate(table=" + type.getSimpleName() + ")");
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
	}

	@Override
	public void onOpen(SQLiteDatabase db) {
		super.onOpen(db);

		if (!db.isReadOnly()) {
			// Enable foreign key constraints
			if (ActiveRecordConfig.FOREIGN_KEYS)
				db.execSQL("PRAGMA foreign_keys=ON;");
		}
	}

	public SQLiteDatabase getReadableDatabase(Class<? extends ActiveRecord> type) {
		SQLiteDatabase db = super.getReadableDatabase();

		if (!isTableCreated(db, getTableName(type)))
			createTable(super.getWritableDatabase(), type);

		return db;
	}

	public SQLiteDatabase getWritableDatabase(Class<? extends ActiveRecord> type) {
		SQLiteDatabase db = super.getWritableDatabase();

		if (!isTableCreated(db, getTableName(type)))
			createTable(db, type);

		return db;
	}

	/**
	 * @param db
	 * @param tableName
	 * @return
	 */
	private boolean isTableCreated(SQLiteDatabase db, String tableName) {
//		if (app.isTableCreated(tableName)) {
//			return true;
//		}

		// SELECT COUNT() FROM sqlite_master WHERE name ='NAME_OF_YOUR_TABLE';

		String[] columns = new String[]{"COUNT()"};
		String[] args = new String[]{tableName};

		Cursor cur = db.query(true, "sqlite_master", columns, "name = ?", args, null, null, null, null);
		if (cur.moveToFirst()) {
			int count = cur.getInt(0);
			if (count > 0) {
//				app.setTableCreated(tableName);
				return true;
			}
		}

		return false;
	}

	/**
	 * @param field
	 * @return
	 */
	private static String getColumnType(Field field) {

		Class fieldType = field.getType();

		String columnType;
		String columnNullable = fieldType.isPrimitive() ? " NOT NULL" : "";

		if (fieldType == Integer.class || fieldType == Long.class || fieldType == Short.class ||
				fieldType == Integer.TYPE || fieldType == Long.TYPE || fieldType == Short.TYPE) {
			columnType = "INTEGER";
		} else if (fieldType == Float.class || fieldType == Double.class ||
				fieldType == Float.TYPE || fieldType == Double.TYPE) {
			columnType = "REAL";
		} else if (fieldType == Character.class || fieldType == Byte.class || fieldType == Boolean.class ||
				fieldType == Character.TYPE || fieldType == Byte.TYPE || fieldType == Boolean.TYPE) {
			columnType = "NUMERIC";
		} else if (fieldType == String.class) {
			columnType = "TEXT";
		} else if (fieldType == Date.class) {
			columnType = "DATE";
		} else if (fieldType.isArray()) {
			// TODO arrays are ignored
			return null;
		} else if (Collection.class.isAssignableFrom(fieldType)) {
			// TODO collections are ignored
			return null;
		} else if (Map.class.isAssignableFrom(fieldType)) {
			// TODO maps are ignored
			return null;
		} else if (Enum.class.isAssignableFrom(fieldType)) {
			// for enums we save the index, obviously this is bad if you change the order...
			columnType = "INTEGER";
		} else if (ActiveRecord.class.isAssignableFrom(fieldType)) {
			// for entities that contain references to other ActiveRecords, we store their id as a long (or SQL INTEGER)
			columnType = "INTEGER";
		} else {
			// TODO all other user object types, serializable objects could be saved?
			return null;
		}

		// overwrite the type if there is an annotation
		if (field.isAnnotationPresent(Column.class)) {
			if (field.getAnnotation(Column.class).type() != null && field.getAnnotation(Column.class).type().length() > 0) {
				columnType = field.getAnnotation(Column.class).type();
			}
			columnNullable = field.getAnnotation(Column.class).nullable() ? "" : " NOT NULL";
		}

		// overwrite the nullable part for the id
		if (field.getName().equalsIgnoreCase("id")) {
			columnNullable = " NOT NULL PRIMARY KEY";
		}

		return new StringBuilder(columnType).append(columnNullable).toString();
	}

	/**
	 * This will create the table associated to the specified active record entity.  This will fail if the table already
	 * exits.
	 *
	 * @param db   the database to create this table in
	 * @param type the class who's table should be created
	 */
	private void createTable(SQLiteDatabase db, Class<? extends ActiveRecord> type) {

		String tableName = getTableName(type);

		StringBuilder sql = new StringBuilder("CREATE TABLE ").append(tableName).append(" (");

		boolean first = true;
		for (Field field : ActiveRecord.getFields(type)) {
			if (!first)
				sql.append(", ");
			first = false;

			sql.append(getColumnName(field)).append(" ").append(getColumnType(field));
		}

		sql.append(" );");

		Log.w(TAG, sql.toString());
		db.execSQL(sql.toString());
//		db.close();

//		app.setTableCreated(tableName);
	}

//	/**
//	 * This will drop the table associated to the specified active record entity.  This will not fail if the table
//	 * doesn't exist.
//	 *
//	 * @param db   the database to create this table in
//	 * @param type the class who's table should be dropped
//	 */
//	private void dropTable(SQLiteDatabase db, Class<? extends ActiveRecord> type) {
//		StringBuilder sql = new StringBuilder("DROP TABLE IF EXISTS ").append(DatabaseHelper.getTableName(type)).append(";");
//
//		Log.w(TAG, sql.toString());
//		db.execSQL(sql.toString());
//		db.close();
//	}

	/**
	 * Returns the plural form of a noun.
	 *
	 * @param name singular
	 * @return plural form of a noun
	 */
	private static String pluralize(String name) {

		name = name.toUpperCase();

		if (irregularBoth.contains(name)) {
			// if name is both singular and plural
			return name;
		} else if (irregularPlural.containsKey(name)) {
			// if name has a special plural form
			return irregularPlural.get(name);
		} else if (name.endsWith("S") || name.endsWith("X") || name.endsWith("CH") || name.endsWith("SH")) {
			return name + "ES";
		} else if (name.endsWith("Y")) {
			return name.substring(0, name.length() - 1) + "IES";
		} else {
			return name + "S";
		}
	}

	/**
	 * Returns the singular form of a noun.
	 *
	 * @param name plural
	 * @return singular form of a noun
	 */
	private static String singularize(String name) {
		name = name.toUpperCase();

		if (irregularBoth.contains(name)) {
			// if name is both singular and plural
			return name;
		} else if (irregularSingular.containsKey(name)) {
			// if name has a special singular form
			return irregularSingular.get(name);
		} else if (!name.endsWith("S")) {
			return name; // oxen?
		} else if (!name.endsWith("ES")) {
			return name.substring(0, name.length() - 1);
		} else if (name.endsWith("SES") || name.endsWith("XES")) {
			return name.substring(0, name.length() - 2);
		} else if (name.endsWith("IES")) {
			return name.substring(0, name.length() - 3) + "Y";
		} else if (name.endsWith("CHES") || name.endsWith("SHES")) {
			return name.substring(0, name.length() - 2);
		} else {
			// should this really happen?
			return name.substring(0, name.length() - 1);
		}
	}

	/**
	 * @param field
	 * @return
	 */
	static String getColumnName(Field field) {
		String annotation = (field.isAnnotationPresent(Column.class)) ? field.getAnnotation(Column.class).name() : null;

		Class fieldType = field.getType();

		// ignore arrays, collections and enums
		if (field.getType().isArray() || Collection.class.isAssignableFrom(field.getType()) || Map.class.isAssignableFrom(field.getType())) {
			return null;
		} else if (annotation != null && annotation.length() > 0) {
			return annotation;
		} else if (ActiveRecord.class.isAssignableFrom(field.getType())) {
			return field.getName() + "_id";
		} else {
			return field.getName();
		}
	}

	/**
	 * Returns the plural name for the specified class.
	 *
	 * @param type the active record class
	 * @return the plural name for the specified active record class
	 */
	static String getTableName(Class<? extends ActiveRecord> type) {

		if (type.isAnnotationPresent(Table.class)) {
			String tableName = type.getAnnotation(Table.class).name();
			if (tableName != null && tableName.length() > 0) {
				return tableName.toUpperCase();
			}
		}
		return pluralize(type.getSimpleName());
	}
}